package momo.app.multicast;

import java.io.IOException;
import java.net.InetAddress;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

import momo.app.comms.CommsManager;
import momo.app.config.AppConfig;
import momo.app.config.ResourceConfig;
import momo.app.status.StatusManager;
import momo.app.structure.Message;
import momo.app.structure.Peer;
import momo.app.util.FileOutput;


/**
 * MulticastComm is a communication library that optimize the structure based on
 * latency, cost and stability.
 * <p>
 * The following is an example of usage:
 * <blockquote><pre>
 *     MulticastComm comm = new MulticastComm();
 *     comm.setCommPort(5125);
 *     comm.setStatusPort(5126);
 *     comm.createProfile();
 *     comm.startListening();
 * </pre></blockquote>
 * <p>
 * This creates an instance of the communication library that listens to port 5125 and 5126 on 
 * Communication and Status Channel respectively.
 * <p>
 * To connect to a client after creation of instance will be as follow:
 * <blockquote><pre>
 *     comm.connect("179.154.69.4", 5126);
 * </pre></blockquote>
 * <p>
 * 
 * @author Jackson Tan
 * @version 1, 02/04/09
 */

public class MulticastComm
{
	/**
	 * The default value of cost.
	 */
	static final double DEFAULT_COST = 0;
	
	/**
	 * The default value for user id.
	 */
	static final String DEFAULT_UID = "";
	
	private MsgRcvListener listener;
	
	private String id;
	private List<InetAddress> listIP;
	private int commPort;
	private int statusPort;
	private double workloadRate;
	private double stability;
	private double cost;
	private Peer me;
	private HashMap<String, Double> userCost;
	
	private ScheduledExecutorService sendScheduler;
	
	/**
	 * Setup the communication library with the given parameters.
	 * @param id          user id
	 * @param commPort    port used for communication channel
	 * @param statusPort  port used for status channel
	 * @param cost        cost of channel communication
	 */
	public MulticastComm(String id, int commPort, int statusPort, double cost)
	{
		this.id = id;
		this.listIP = ResourceConfig.getIPAddress();
		this.commPort = commPort;
		this.statusPort = statusPort;
		this.workloadRate = 1;
		this.stability = 1;
		this.cost = cost;
		
		userCost = new HashMap<String, Double>();
		sendScheduler = Executors.newScheduledThreadPool(1);
	}
	
	/**
	 * Setup the communication library with the given parameters. Cost will be based on default values of 0.
	 * @param id          user id
	 * @param commPort    port used for communication channel
	 * @param statusPort  port used for status channel
	 */
	public MulticastComm(String id, int commPort, int statusPort)
	{
		this(id, commPort, statusPort, DEFAULT_COST);
	}
	
	/**
	 * Setup the communication library with the given parameters.
	 * Id will be based on default value of <IP_Address>:<StatusPort>:<Generated_Random_No>
	 * Cost will be based on default values of 0.
	 * @param commPort    port used for communication channel
	 * @param statusPort  port used for status channel
	 */
	public MulticastComm(int commPort, int statusPort)
	{
		this(DEFAULT_UID, commPort, statusPort, DEFAULT_COST);
	}
	
	/**
	 * Setup the communication library with the given parameters.
	 * CommunicationPort and StatusPort will be based on the values in the configuration file.
	 * Cost will be based on default values of 0.
	 * @param id user id
	 */
	public MulticastComm(String id)
	{
		this(id, ResourceConfig.getCommPort(), ResourceConfig.getStatusPort(), DEFAULT_COST);
	}
	
	/**
	 * Setup the communication library with all default values.
	 */
	public MulticastComm()
	{
		this(DEFAULT_UID, ResourceConfig.getCommPort(), ResourceConfig.getStatusPort(), DEFAULT_COST);
	}
	
	/**
	 * Set the port for communication channel
	 * @param commPort port used for communication channel
	 */
	public void setCommPort(int commPort)
	{
		this.commPort = commPort;
	}
	
	/**
	 * Set the port for status channel
	 * @param statusPort port used for status channel
	 */
	public void setStatusPort(int statusPort)
	{
		this.statusPort = statusPort;
	}
	
	/**
	 * Set the cost of channel communication
	 * @param cost cost of channel communication
	 */
	public void setCost(double cost)
	{
		this.cost = cost;
	}
	
	/**
	 * States the cost to that particular client
	 * @param source source (user id) of the client
	 * @param cost cost of the communication
	 */
	public void setUserCost(String source, double cost)
	{
		userCost.put(source, cost);
	}
	
	public void setUserCost(Map<String, Double> costMap) {
		for ( String sourceId : costMap.keySet() )
			userCost.put( sourceId, costMap.get(sourceId) );
	}
	
	/**
	 * Invoking this method will create a user profile based on the given values of the constructor.
	 * This method is required to invoke after constructor is called.
	 */
	private void createProfile()
	{
		if ( id.trim().equals("") )
			me = new Peer(listIP, commPort, statusPort, workloadRate, stability, cost);
		else
			me = new Peer(id, listIP, commPort, statusPort, workloadRate, stability, cost);
		
		System.out.println("Code did run!!");
		if ( me == null )
			System.out.println("I m null");
		else
			System.out.println(me.toString());
		
		AppConfig.getInstance().setMe(me);
		
		Iterator<String> sourceIter = userCost.keySet().iterator();
		while ( sourceIter.hasNext() )
		{
			String source = sourceIter.next();
			double cost = userCost.get(source);
			AppConfig.getInstance().setUserCost(source, cost);
		}
	}
	
	/**
	 * Starts listening on Communication and Status channel.
	 */
	private void startListening()
	{
		StatusManager statusManager = StatusManager.getInstance();
		CommsManager commsManager = CommsManager.getInstance();
		
		statusManager.listen();
		commsManager.listen();
	}
	
	/**
	 * Connects to the client
	 * @param host          IP Address of the client
	 * @param statusPort    port of the status channel of the client
	 * @throws IOException  if connection to the client fails
	 */
	public void connect(String host, int statusPort) throws IOException
	{
		this.createProfile();
		this.startListening();
		
		if ( !host.trim().equals("") && host != null && statusPort > 0 )
		{
			InetAddress ip = InetAddress.getByName(host);
			StatusManager.getInstance().joinRequest(ip, statusPort);
			
			while ( !CommsManager.getInstance().getSafeJoin() )
			{
				CommsManager.getInstance().ping();
				try
				{
					Thread.sleep(1000);
				}catch(Exception e){}
			}
			CommsManager.getInstance().resetSafeJoin();
		}
	}
	
	/**
	 * Leaves the network.
	 */
	public void leave()
	{
		StatusManager.getInstance().stopAllComms();
		CommsManager.getInstance().stopAllComms();
	}
	
	/**
	 * This will send the data to connected clients in the network
	 * Sent contents will be tracked through the usage of underlying state vector.
	 * If data are sent and lost using this method, the user will be informed through MsgRcvListener.
	 * Note: When the library encounters problem in the synchronization of message states, 
	 * messages that are sent using this method will be hold till all issues are resolved.
	 * @param contents data to send
	 */
	public void synchronizedSend(String contents)
	{
		send(contents, true);
	}
	
	/**
	 * This will send data to connected clients in the network without 
	 * any reliability assurance from the library. Message will not be hold back in any case.
	 * @param contents data to send
	 */
	public void send(String contents)
	{
		send(contents, false);
	}
	
	/**
	 * Invoking this method will reset the underlying state vector of the library
	 */
	public void reset()
	{
		CommsManager.getInstance().resetState();
	}
	
	/**
	 * Allows user to send data with the choice of reliability 
	 * @param contents data to send
	 * @param synchronizedRequired true if required the reliability of library. 
	 */
	private void send(String contents, boolean synchronizedRequired)
	{
		sendScheduler.schedule(
				new SendTask(contents, synchronizedRequired),
				0, 
				TimeUnit.MILLISECONDS);
		
//		Random rand = new Random();
//		int seed = rand.nextInt();
//		FileOutput.getInstance().logToFile("test.log", "~~~~~~~~~~ be4: " + seed);
//		
//		Message msg = new Message(Message.NO_FORWARD, AppConfig.getInstance().getId(), Message.USER_DATA, contents);
//		CommsManager.getInstance().sourceForward(msg, synchRequired);
//		
//		FileOutput.getInstance().logToFile("test.log", "~~~~~~~~~~ after: " + seed);
	}
	
	/**
	 * Register the listener
	 * @param newListener 
	 */
	public void addMsgRcvListener(MsgRcvListener newListener)
	{
		if ( listener != null )
			throw new IllegalStateException("Listener has already been registered");
		
		listener = newListener;
		CommsManager.getInstance().setMsgListener(listener);
	}
	
}//end of class MulticastComm
